<?php
/*
 用PHP实现的服务器，依赖swoole扩展
 直接做为裸露服务器可能会出现某些无法预料的问题
 可以在前面加一级nginx，以实现类似于nginx+php-fpm的功能
 性能上比nginx+php-fpm强大一些。
*/
 namespace Lib;
 use \Lib\Parser;
 use \Lib\EchoLog;
class HttpServer{
    // 缓冲
    protected $buffers = [];
    protected $body_size = [];
    // 请求解析器
    protected $parser;
    // 配置
    public $config;
    // 请求回调函数
    protected $_onRequest;
    // 当前客户端ID
    protected $currentFd;
    // 发送内容头
    protected $headerInfo = [];
    // \swoole_server
    public $serv;
    // 编码
    protected $charset = 'utf-8';
    // 记录日志类
    protected $loger;
    // pid文件
    public $pid_file;
    // cookie
    protected $cookies = [];
    // 插件
    public $plugins = [];

    /**
    * 是否启用了子进程，如果启用，则在shutdown_function处理函数中，要忽略向客户端输出，
    * 否则子进程退出，会调用shutdown_function函数向客户端输出数据，然后关闭链接，导致后面的正常输出被忽略
    */
    public $has_process = false;

    // 各种状态
    static $HttpStatus = array(
        100 => "100 Continue",
        101 => "101 Switching Protocols",
        200 => "200 OK",
        201 => "201 Created",
        204 => "204 No Content",
        206 => "206 Partial Content",
        300 => "300 Multiple Choices",
        301 => "301 Moved Permanently",
        302 => "302 Found",
        303 => "303 See Other",
        304 => "304 Not Modified",
        307 => "307 Temporary Redirect",
        400 => "400 Bad Request",
        401 => "401 Unauthorized",
        403 => "403 Forbidden",
        404 => "404 Not Found",
        405 => "405 Method Not Allowed",
        406 => "406 Not Acceptable",
        408 => "408 Request Timeout",
        410 => "410 Gone",
        413 => "413 Request Entity Too Large",
        414 => "414 Request URI Too Long",
        415 => "415 Unsupported Media Type",
        416 => "416 Requested Range Not Satisfiable",
        417 => "417 Expectation Failed",
        500 => "500 Internal Server Error",
        501 => "501 Method Not Implemented",
        503 => "503 Service Unavailable",
        506 => "506 Variant Also Negotiates"
    );

    public function __construct($config){
        $this->pid_file = dirname(__DIR__).'/logs/'.APP_NAME.'.pid';
        $this->config = $config;
        $this->loger = new EchoLog('');
        register_shutdown_function(array($this, 'handleFatal'));
    }

    /**
    * 运行服务器
    * @access public
    * @return void
    */    
    public function run(){
        $swcfg = array_merge(
            [
                'log_file' => dirname(__DIR__).'/logs/'.APP_NAME.'.log',
                'worker_num' => 8,
                'max_request' => 100000,
                'max_conn' => 10000,
                'daemonize' => 0,
            ],$this->c('global'));
       
        $server = new \swoole_server($this->c('server.host'), $this->c('server.port'));
        $this->serv = $server;
        $server->set($swcfg);
        $server->on('Start',array($this,'onStart'));
        $server->on('ManagerStart', array($this,'onManagerStart'));
        $server->on('WorkerStart',array($this,'onWorkerStart'));
        $server->on('Connect', array($this, 'onConnect'));
        $server->on('Receive', array($this, 'onReceive'));
        $server->on('Close', array($this, 'onClose'));
        $server->on('Shutdown', array($this, 'onShutdown'));
        $server->on('Task', array($this, 'onTask'));
        $server->on('Finish', array($this, 'onFinish'));
        $server->on('WorkerStop',[$this,'onWorkerStop']);
        $server->on('WorkerError',[$this,'onWorkerError']);
        if($this->c('server.enable_timer')){
            $server->on('Timer',[$this,'onTimer']);
        }
        $server->start();
    }

    /**
    * 主进程启动时回调函数
    * @access public
    * @param \swoole_server $serv
    * @return void
    */
    public function onMasterStart($serv){
        $this->log(SERVER_NAME."[#master] start");
    }

    /**
    * 管理进程启动时回调函数
    * @access public
    * @param \swoole_server $serv
    * @return void
    */
    public function onManagerStart($serv){
        $this->setProcessName('php-manager '.APP_NAME.' ('.__DIR__.'/etc/'.APP_NAME.'.ini)');
        $this->log(SERVER_NAME."[#manager] start");
    }

    /**
    * 服务器关闭时回调函数
    * @access public
    * @param \swoole_server $serv
    * @return void
    */
    public function onShutdown($serv){
        is_file($this->pid_file) && unlink($this->pid_file);
        exec('rm -rf '.SHM_PATH);
        $this->log(SERVER_NAME." shutdown");
        apply_action('on_shutdown',$this,$serv);
    }

    /**
    * 服务器启动时回调函数
    * @access public
    * @param \swoole_server $serv
    * @return void
    */
    public function onStart($serv){
        $this->loadPlugin();
        $this->setProcessName('php-master ' . APP_NAME . ' host=' . $this->config['server']['host'] . ' port=' . $this->config['server']['port']);
        $this->c('global.daemonize') && file_put_contents($this->pid_file, $serv->master_pid);
        apply_action('server_start',$serv);
    }

    /**
    * work进程启动时回调函数
    * @access public
    * @param \swoole_server $server
    * @param int $worker_id
    * @return void
    */
    public function onWorkerStart($server,$worker_id){
        ($user = $this->c('server.user')) && $this->changeUser($user);
        if ($worker_id >= $this->c('global.worker_num')){
            $this->setProcessName('php-task '.APP_NAME);
            $this->log("php-task[#{$worker_id}] running on ".$this->c('server.host').":".$this->c('server.port'));
        }else{
            $this->worker_id = $worker_id;
            $this->setProcessName('php-worker '.APP_NAME);
            $this->log("php-worker[#{$worker_id}] running on ".$this->c('server.host').":".$this->c('server.port'));
            $this->loadPlugin();
            apply_action('on_worker_start',$server,$worker_id);
        }
    }

    /**
    * work进程退出时执行
    * @access public
    * @param string $var
    * @return void
    */
    public function onWorkerStop($serv,$worker_id){
        $this->log("php-worker[#{$worker_id}] Stoped");
        while(\swoole_process::wait());
    }

    /**
    * 当work遇到错误时
    * @access public
    * @param swoole_server $serv
    * @return void
    */
    public function onWorkerError($serv,$worker_id,$worker_pid,$exit_code){
        $this->log("php-worker[#{$worker_id}] Error #{$exit_code}");
    }

    /**
    * 异步任务回调函数
    * @access public
    * @param \swoole_server $serv
    * @param int $task_id
    * @param int $from_id
    * @param string $data
    * @return void
    */
    public function onTask($serv, $task_id, $from_id, $data){
        try{
            $arr = explode('|',$data,2);
            $task_name = $arr[0];
            $params = '';
            if(isset($arr[1])){
                $params = unserialize($arr[1]);
            }
            $task = '\\Task\\'.ucwords($task_name);
            $task::run($params);
        }catch(\Exception $e){
            $this->log($e->getMessage());
        }
        // $serv->finish();
    }

    /**
    * 异步任务结束时回调函数
    * @access public
    * @param \swoole_server $var
    * @param int $task_id
    * @param string $data
    * @return void
    */
    public function onFinish($serv, $task_id, $data){
        echo 'task data:'.$data;
    }

    /**
    * 定时器
    * @access public
    * @param \swoole_server $serv
    * @param int $interval 毫秒数
    * @return void
    */
    public function onTimer($serv,$interval){
        apply_action('on_timer',$serv,$interval);
    }

    /**
    * 新连接到来时回调函数
    * @access public
    * @param \swoole_server $serv
    * @param int $client_id
    * @param from_id $from_id
    * @return void
    */
    public function onConnect($serv, $client_id, $from_id){
        apply_action('on_connect',$this,$serv,$client_id,$from_id);
    }

    /**
     * 接收数据回调函数
     * @param \swoole_server $serv
     * @param int $fd
     * @param int $from_id
     * @param string $data
     */
    public function onReceive($serv, $fd, $from_id, $data){
        if(!isset($this->buffers[$fd])){
            $this->parser = new Parser;
            $this->currentFd = $fd;
            $this->from_id = $from_id;
            $this->buffers[$fd] = '';
            $this->body_size[$fd] = 0;
            $this->ph=false;
            $this->method = '';
            $this->content_length = 0;
        }
        
        $this->buffers[$fd] .= $data;
        if(!$this->ph && strpos($this->buffers[$fd],"\r\n\r\n")!==false){
            $this->ph = true;
            list($header,$_) = explode("\r\n\r\n",$this->buffers[$fd],2);
            foreach (explode("\r\n",$header) as $key=>$line) {
                if($key==0){
                    $this->method = explode(" ",$line,2)[0];
                }else{
                    list($key,$val) = explode(":",$line);
                    if($key=="Content-Length"){
                        $this->content_length = $val;
                        break;
                    }
                }
            }
        }
        if($this->method=='POST'){
            $this->body_size[$fd] += strlen($data);
            if($this->body_size[$fd]<$this->content_length){
                return;
            }
        }
        $this->parser->execute($this->buffers[$fd]);

        if($this->parser->isFinished()){
            if(defined('DEBUG')){
                $this->log("new request:\"".$_SERVER['HTTP_METHOD'].' '.$_SERVER['REQUEST_URI']."\" fd={$fd} from={$from_id}");
            }
            $info = $serv->connection_info($fd);
            $_SERVER['CXSV_INFO'] = $info;
            $_SERVER['REMOTE_ADDR'] = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $info['remote_ip'];
            $this->clear($fd);
            call_user_func($this->_onRequest,$serv,$fd,$from_id);
        }elseif($this->parser->hasError()){
            $this->response($this->serv,$this->currentFd,$this->parser->getError(),400);
            $serv->close($fd);
        }
    }

    /**
    * 处理请求回调函数
    * @access public
    * @param function $callback
    * @return void
    */
    public function onRequest($callback){
        $this->_onRequest = $callback;
    }

    /**
     * 连接关闭时回调函数
     * @param \swoole_server $serv
     * @param int $fd
     * @param int $from_id
     * @return void
     */
    public function onClose($serv, $fd, $from_id){
        $this->clear($fd);
        if(defined('DEBUG')) $this->log("client[#$fd@$from_id] close");
    }

    /**
    * 更改进程名称
    * @access public
    * @param string $name
    * @return void
    */
    public function setProcessName($name){
        swoole_set_process_name($name);
    }

    /**
    * 获得配置项
    * @access public
    * @param string $key
    * @return string
    */
    public function c($key){
        $key = explode('.',$key);
        $val = isset($this->config[$key[0]]) ? $this->config[$key[0]] : '';
        if(isset($key[1])){
            isset($val[$key[1]]) && ($val = $val[$key[1]]) || ($val = '');
        }
        return $val;
    }

    /**
    * 清理
    * @access protected
    * @param int $fd
    * @return void
    */
    protected function clear($fd){
        unset($this->buffers[$fd]);
        unset($this->cookies[$fd]);
        $this->headerInfo = [];
        $this->parser = null;
    }

    /**
     * 改变进程的用户ID
     * @param string $user
     * @return void
     */
    public function changeUser($user){
        if (!function_exists('posix_getpwnam')){
            trigger_error(__METHOD__.": require posix extension.");
            return;
        }
        $user = posix_getpwnam($user);
        if($user){
            posix_setuid($user['uid']);
            posix_setgid($user['gid']);
        }
    }

    /**
    * 加载插件
    * @access private
    * @return void
    */
    private function loadPlugin(){
        $paths = array('global'=>FRAME_ROOT.'plugin/','app'=>DOCUMENT_ROOT.'Plugin/');
        foreach($paths as $type => $plugin_path){
            if(!is_dir($plugin_path)){
                continue;
            }
            $odir = opendir($plugin_path);
            while($file = readdir($odir)){
                if($file{0}!='.' && ($path=$plugin_path.$file.'/') && ($plugin_file=$path.$file.'.php') && is_file($plugin_file) && (is_file($path.'enabled') || $type == 'global')){
                    include $plugin_file;
                }
            }
        }
    }

    /**
    * 输入404错误
    * @access public
    * @return void
    */
    public function http404(){
        return $this->error('404 Not Found','你所请求的页面已去了火星，不再回来了~~',404);
    }

    /**
    * 返回302状态
    * @access public
    * @param string $url
    * @return void
    */
    public function http302($url){
        $response = array(
            'HTTP/1.1 302',
        );
        $response[] = 'Location:'.$url;
        $response[] = 'Content-Length:0';
        if(isset($this->cookies[$this->currentFd])){
            foreach($this->cookies[$this->currentFd] as $cookie){
                $response[]= "Set-Cookie:".$cookie;
            }
        }
        $response[] = '';
        $response[] = '';
        $this->serv->send($this->currentFd, join("\r\n",$response));
        //添加这一句，是为了终止程序继续执行
        throw new \Exception("", 1);
    }

    /**
    * 设置发送内容头部
    * @access public
    * @param string $key
    * @param string $value
    * @return void
    */
    public function header($key, $value){
        $this->headerInfo[$key] = $value;
    }

    /**
    * 设置发送内容的cookie
    * @access public
    * @param string $name
    * @param string $value
    * @param int $expires
    * @param string $path
    * @param string $domain
    * @return void
    */
    public function setCookie($name,$value,$expires=0,$path='/',$domain=''){
        $this->cookies[$this->currentFd][] = "{$name}=".trim($value).";".($expires ? "expires=".gmdate("D, d-M-Y H:i:s",time()+$expires)." GTM;" : "")."path={$path};domain={$domain}";
    }

    /**
    * 发送内容
    * @access public
    * @param \swoole_server $serv
    * @param int $fd
    * @param string $respData
    * @param int $code
    * @return void
    */
    public function response($serv,$fd,$respData, $code = 200){
        $response = array(
            'HTTP/1.1 '.self::$HttpStatus[$code],
        );
        
        $headers = array(
            'Cache-Control'=>'must-revalidate,no-cache',
            'Content-Language'=>'zh-CN',
            'Server'=>SERVER_NAME,
            'Content-Type'=>'text/html;charset='.$this->charset,
            'Content-Length'=>strlen($respData),
        );
        $headers  = array_merge($headers,$this->headerInfo);
        foreach($headers as $key=>$val){
            $response[] = $key.':'.$val;
        }
        if(isset($this->cookies[$fd]) && !empty($this->cookies[$fd])){
            foreach($this->cookies[$fd] as $cookie){
                $response[]= "Set-Cookie:".$cookie;
            }
        }
        $response[] = '';
        $response[] = $respData;
        $send_data = join("\r\n",$response);
        $data_length = strlen($send_data);
        if($data_length>=2*1024*1024){
            $tmp_file = tempnam('/dev/shm', 'sw_tmp_'.$fd.'_');
            file_put_contents($tmp_file, $send_data);
            try{
                $serv->sendfile($fd,$tmp_file);
            }catch(\Exception $e){
                echo $e->getMessage();
                $response = array_pop($response);
                $serv->send($fd,join("\r\n",$response));
            }finally{
                // 等待3秒，待sendfile发送完毕再删除文件，否则会出错
                swoole_timer_after(3000,function() use($tmp_file,$serv,$fd){
                    $serv->close($fd);
                    @unlink($tmp_file);
                });
            }
        }else{
            $serv->send($fd, $send_data);
        }
        unset($send_data,$response);
    }


    /**
    * 写日志
    * @access public
    * @param string $msg
    * @return void
    */
    public function log($msg){
        $this->loger->info($msg);
    }

    /**
    * 发送错误信息
    * @access public
    * @param string $msg
    * @param string $content
    * @param int $code
    * @return void
    */
    public function error($msg,$content,$code = 200){
        $SERVER_NAME = SERVER_NAME;
        $str = <<<EOF
    <!DOCTYPE html>
    <html>
    <head>
        <title>{$msg}</title>
    </head>
    <body>
    <h1>{$msg}</h1><p>{$content}</p><hr>power by {$SERVER_NAME}
    </body>
    </html>
EOF;
        $this->response($this->serv,$this->currentFd,$str,$code);
    }

    /**
     * Fatal Error的捕获
     * @access public
     * @return void
     */
    public function handleFatal(){
        $error = error_get_last();
        $data = '';
        if(ob_get_contents()){
            $data = ob_get_contents();
            ob_end_clean();
        }
        /**
        * 如果脚本退出时，检测到has_process为true，说明是子进程退出
        * 此时直接echo写到日志，而不是向客户端发送数据
        */
        if($this->has_process){
            if(!empty($data) || !empty($error)){
                $msg = $data.$error['message'];
                echo $msg;
            }
            return;
        }
        if (empty($error) || !isset($error['type'])){
            $new_data = json_decode($data,true);
            if(gettype($new_data)=='array'){
                $new_data['_worker_info'] = 'worker restarted';
                $data = json_encode($new_data);
            }else{
                $data .= '<font color="red">worker restarted</font>';
            }
            $this->currentFd && $this->response($this->serv,$this->currentFd,$data);
            return;
        }else{
            $error['file'] = explode('.',$error['file'])[0];
            $errorMsg = "{$error['message']} (".str_replace([SHM_PATH.'tpls/',SHM_PATH.'widget/tpl/',APP_PATH,FRAME_ROOT,DOCUMENT_ROOT],['tpl:','widget_tpl:','app:','root:','app:'],$error['file']).":{$error['line']})";
            $message = \Lib\Error::info("Server fatal error", $errorMsg);
            if($this->currentFd){
                $this->response($this->serv,$this->currentFd,$message);
            }else{
                print_r($message);
            }
        }
    }
}
